#include "FileUtils.h"

#include <dirent.h>

#include <algorithm>
#include <chrono>
#include <fstream>
#include <iostream>

namespace {
	struct FileInformation {
		FileInformation(const std::string& _name) : name(_name), size(getFileSize(_name)), hash(calcHashForFile(_name, 1 << 12)) {};
		std::string name;
		long long int size;
		int hash;
	};

	bool operator < (const FileInformation& a, const FileInformation& b) {
		return a.size < b.size || a.size == b.size && a.hash < b.hash ||
			a.size == b.size && a.hash == b.hash && a.name < b.name;
	}

	bool hasEqualHashAndSize(const FileInformation& a, const FileInformation& b) {
		return a.hash == b.hash && a.size == b.size;
	}
};

bool isFile(const std::string& fileName) {
	struct stat st;
	if (stat(fileName.c_str(), &st) == -1) {
		return false;
	}
	return S_ISDIR(st.st_mode) == false;
}

bool isDirectory(const std::string& dirName) {
	struct stat st;
	if (stat(dirName.c_str(), &st) == -1) {
		return false;
	}
	return S_ISDIR(st.st_mode) == true;
}

void getAllFilesInDirectory(std::vector<std::string>* result, const std::string& dirName) {
	if (result == nullptr) {
		return;
	}
	if (isDirectory(dirName) == false) {
		if (isFile(dirName)) {
			result->push_back(dirName);
		}
		return;
	}

	DIR* dir = opendir(dirName.c_str());
	if (dir == nullptr) {
		return;
	}

	while (true) {
		struct dirent* curDir = readdir(dir);
		if (curDir == nullptr) {
			break;
		}
	
		if (strcmp(curDir->d_name, ".") == 0 || strcmp(curDir->d_name, "..") == 0) {
			continue;
		}

		getAllFilesInDirectory(result, dirName + "\\" + curDir->d_name);
	}
	closedir(dir);
}

long long int getFileSize(const std::string& fileName) {
	FILE* file;
	fopen_s(&file, fileName.c_str(), "rb");
	if (file == nullptr) {
		return 0;
	}

	fseek(file, 0, SEEK_END);
	long long int size = ftell(file);
	fclose(file);

	return size;
}

int calcHashForFile(const std::string& fileName, long long int maxSymbolsToRead) {
	if (!isFile(fileName)) {
		return -1;
	}

	const int base = 257;
	const int mod = 1237435867;
	int hash = 0;

	FILE* file;
	fopen_s(&file, fileName.c_str(), "rb");
	if (file == nullptr) {
		return -1;
	}

	try {
		fseek(file, 0, SEEK_END);
		long long int leftToRead = ftell(file);
		rewind(file);
		leftToRead = min(leftToRead, maxSymbolsToRead);

		const int kBufferSize = static_cast<int>(min(1LL << 22, leftToRead));
		char* buffer =  new char[kBufferSize + 1];

		while (leftToRead > 0) {
			int lenToRead = kBufferSize;
			if (lenToRead > leftToRead) {
				lenToRead = static_cast<int>(leftToRead);
			}

			fread(buffer, sizeof(char), lenToRead, file);
			leftToRead -= lenToRead;
		
			for (int i = 0; i < lenToRead; ++i) {
				hash = (1LL * hash * base + buffer[i]) % mod;
			}
		}

		fclose(file);
		delete[] buffer;
	} catch(...) {
		return -1;
	}

	return hash;
}

int compareTwoFiles(const std::string& firstFile, const std::string& secondFile) {
	enum EResult {R_NOT_EQUAL = 0, R_EQUAL = 1, R_FAILED_TO_COMPARE = 2};
	if (!isFile(firstFile) || !isFile(secondFile)) {
		return R_FAILED_TO_COMPARE;
	}

	FILE* file1;
	fopen_s(&file1, firstFile.c_str(), "rb");
	FILE* file2;
	fopen_s(&file2, secondFile.c_str(), "rb");
	if (file1 == nullptr || file2 == nullptr) {
		return R_FAILED_TO_COMPARE;
	}

	try {
		fseek(file1, 0, SEEK_END);
		long long int firstFileSize = ftell(file1);
		rewind(file1);
	
		fseek(file2, 0, SEEK_END);
		long long int secondFileSize = ftell(file2);
		rewind(file2);		

		if (firstFileSize != secondFileSize) {
			return R_NOT_EQUAL;
		}

		const int kBufferSize = static_cast<int>(min(1LL << 22, firstFileSize));
		char * buffer1 = new char[kBufferSize + 1];
		char * buffer2 = new char[kBufferSize + 1];

		long long int leftToRead = firstFileSize;
		while (leftToRead > 0) {
			int lenToRead = kBufferSize;
			if (lenToRead > leftToRead) {
				lenToRead = static_cast<int>(leftToRead);
			}

			fread(buffer1, sizeof(char), lenToRead, file1);
			buffer1[lenToRead] = '\0';
			fread(buffer2, sizeof(char), lenToRead, file2);
			buffer2[lenToRead] = '\0';
			leftToRead -= lenToRead;

			if (memcmp(buffer1, buffer2, sizeof(buffer1)) != 0) {
				return R_NOT_EQUAL;
			}
		}

		fclose(file1);
		fclose(file2);
		delete[] buffer1;
		delete[] buffer2;
	} catch(...) {
		return R_FAILED_TO_COMPARE;
	}
	return R_EQUAL;
}

void scanDirectory(const std::string& startDirectory, const std::string& logFileName) {
	auto startTime = std::chrono::high_resolution_clock::now();
	std::vector<std::string> allFileNames;
	getAllFilesInDirectory(&allFileNames, startDirectory);
	auto scanFinishTime = std::chrono::high_resolution_clock::now();

	if (logFileName != "") {
		FILE* stream;
		freopen_s(&stream, logFileName.c_str(), "w", stdout);
	}

	std::ios::sync_with_stdio(false);
	std::cout << "Found " << allFileNames.size() << " files" << std::endl;
	std::cout << "Time for scan: "
	   		  << std::chrono::duration_cast<std::chrono::milliseconds>(
				scanFinishTime - startTime).count() << "ms" << std::endl;

	std::vector<FileInformation> allFiles;
	for (size_t i = 0; i < allFileNames.size(); ++i) {
		allFiles.push_back(FileInformation(allFileNames[i]));
	}
	sort(allFiles.begin(), allFiles.end());
	auto readFinishTime = std::chrono::high_resolution_clock::now();

	std::cout << "Read all files." << std::endl;
	std::cout << "Time for read: "
			  << std::chrono::duration_cast<std::chrono::milliseconds>(
				readFinishTime - scanFinishTime).count() << "ms" << std::endl;

	std::vector<std::vector<std::string>> filesByCluster;
	std::vector<bool> usedFiles(allFiles.size(), false);

	for (size_t i = 0; i < allFiles.size(); ++i) {
		if (usedFiles[i]) {
			continue;
		}
		usedFiles[i] = true;
		filesByCluster.push_back(std::vector<std::string>(1, allFiles[i].name));
		for (size_t j = i + 1; j < allFiles.size(); ++j) {
			if (hasEqualHashAndSize(allFiles[i], allFiles[j]) == false) {
				break;
			}
			if (compareTwoFiles(allFiles[i].name, allFiles[j].name) == 1) {
				usedFiles[j] = true;
				filesByCluster.back().push_back(allFiles[j].name);
			}
		}
	}
	auto clusteringFinishTime = std::chrono::high_resolution_clock::now();

	std::cout << "Time for clustering: "
			  << std::chrono::duration_cast<std::chrono::milliseconds>(
				clusteringFinishTime - readFinishTime).count() << "ms" << std::endl;
	std::cout << "Total time: "
			  << std::chrono::duration_cast<std::chrono::milliseconds>(
				clusteringFinishTime - startTime).count() << "ms" << std::endl << std::endl;

	std::cout << "All files from directory: " << startDirectory << std::endl;
	for (size_t i = 0; i < filesByCluster.size(); ++i) {
		std::cout << std::endl;
		std::cout << "Cluster " << i << " has "
					<< filesByCluster[i].size() << " file(s)." << std::endl;
		for (size_t j = 0; j < filesByCluster[i].size(); ++j) {
			std::cout << filesByCluster[i][j] << std::endl;
		}
	}
	std::ios::sync_with_stdio(true);
}